/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package game.experts.combine;

import analysys.HECAlignment;
import analysys.HECAlignmentHit;
import game.datahandling.IOData;
import game.datahandling.dataenvelopes.Data;
import game.datahandling.dataenvelopes.HECProtein;
import game.datahandling.dataenvelopes.base.PrimaryStructure;
import game.encodings.OutputEncoding;
import game.experts.Expert;
import java.util.ArrayList;
import java.util.List;
import utils.Profile;
import utils.ProfileMatrix;

/**
 *
 * @author emanuele
 */
public class SSPHintedWindowCombinerExpert extends HomogeneousCombinerExpert<PrimaryStructure> {

    protected class CurrentInfo {
        public PrimaryStructure input;
        // Una riga per ogni finestra, una colonna per ogni hint.
        public List<List<HECAlignmentHit>> windowHints;
        public int windowNumber;
        public int windowSize;
    }

    public int hints = 3;
    public double minevalueth = 0;

    public int maximumWindowNumber = 10;
    public int minimumWindowSize = 25;

    public String before = "";
    public String after  = "";
    
    protected CurrentInfo current = null;

    @Override
    public OutputEncoding predict(PrimaryStructure input) {
        if (this.current == null) {
            this.current = new CurrentInfo();
            this.current.windowNumber = this.maximumWindowNumber;
        }
        
        this.current.input = input;

        this.current.windowSize = this.current.input.length() / this.current.windowNumber;
        if (this.current.windowSize < this.minimumWindowSize) {
            this.current.windowSize = this.minimumWindowSize;
            this.current.windowNumber = this.current.input.length() / this.current.windowSize;
        }

        this.current.windowHints = this.getWindowAlignmentHints(input.toString());

        return super.predict(input);
    }

    @Override
    public double getCompetence(PrimaryStructure d) {
        return 1.0;
    }

    List<HECAlignmentHit> getAlignmentHints(String input) {
        List<HECAlignmentHit> ret = new ArrayList<HECAlignmentHit>();

        HECAlignment al = new HECAlignment(new HECProtein(input), 1e-2);

        for (int i = 0; i < al.size() && ret.size() < this.hints; i++) {
            HECAlignmentHit hint = al.get(i);
            if (hint.evalue >= this.minevalueth) {
                ret.add(hint);
            }
        }

        return ret;
    }

    List<List<HECAlignmentHit>> getWindowAlignmentHints(String input) {
        List<List<HECAlignmentHit>> ret = new ArrayList<List<HECAlignmentHit>>();

        for (int win = 0; win < this.current.windowNumber; win++) {
            int startIdx, endIdx;

            startIdx = win*this.current.windowSize;
            if (win + 1 != this.current.windowNumber) {
                // Prendiamo il punto finale della finestra
                endIdx = (win+1)*this.current.windowSize;
            } else {
                // Il punto finale è la fine della stringa
                endIdx = input.length();
            }

            ret.add(this.getAlignmentHints(input.substring(startIdx, endIdx)));
        }

        return ret;
    }

    public boolean canPredict(IOData<PrimaryStructure, Data> data) {
        return true; // TODO
    }

    protected int getDifferences(String a, String b) {
        assert a.length() == b.length();

        int ret = 0;
        for (int i = 0; i < a.length(); i++) {
            if (a.charAt(i) != b.charAt(i)) {
                ret++;
            }
        }
        return ret;
    }
    
    protected List<String> alignWindowHintPredictions(Expert<PrimaryStructure> e, int window) {
        List<String> ret = new ArrayList<String>();

        List<HECAlignmentHit> hintList = this.current.windowHints.get(window);

        for (HECAlignmentHit hint: hintList) {
            String hapri  = before + hint.hitAlignedPri.replaceAll("-", "X") + after;
            String hwpred = e.predict(new PrimaryStructure(hapri)).decode().toString();

            StringBuffer hawpred = new StringBuffer("");
            for (int i = 0; i < hint.hitAlignedPri.length(); i++) {
                if (hint.hitAlignedPri.charAt(i) == '-')
                    hawpred.append('-');
                else
                    hawpred.append(hwpred.charAt(i + before.length()));
            }

            ret.add(hawpred.toString());
        }

        return ret;
    }

    private List<Double> getExpertWeights(Expert e) {
        List<Double> weights = new ArrayList<Double>();

        if (this.current.windowHints.get(0).isEmpty()) {
            // Non c'è allineamento: tutti i pesi sono uguali.
            for (int i = 0; i < this.current.windowNumber; i++) {
                weights.add(1.0);
            }
            return weights;
        }

        for (int win = 0; win < this.current.windowNumber; win++) {
            List<String> alignedWindowHintPredictions = this.alignWindowHintPredictions(e, win);
            
            for (int hintIdx = 0; hintIdx < alignedWindowHintPredictions.size(); hintIdx++) {
                String awhp = alignedWindowHintPredictions.get(hintIdx);
                String awhs = this.current.windowHints.get(win).get(hintIdx).hitAlignedSec;
                double realLength = 0;
                double weight;

                for (int i = 0; i < awhp.length(); i++) {
                    if (awhp.charAt(i) == '-')
                        continue;
                    realLength++;
                }

                if (realLength == 0) {
                    // La finestra contiene solo '-', quindi il peso è uno.
                    weight = 1.0;
                } else {
                    double errors = this.getDifferences(awhp, awhs);
                    errors += 0.5; // Per evitare errorRate == 0

                    double errorRate = errors/realLength;

                    weight = Math.pow(Math.exp(1.0 / errorRate), 2);
                }

                // Se siamo al primo hint inserisci il peso, sennò aggiornalo
                // C'è un peso per finestra!
                if (hintIdx == 0) {
                    weights.add(weight);
                } else {
                    double prevWeight = weights.get(win);
                    weights.set(win, prevWeight + weight);
                }
            }
        }

        return weights;
    }

    @Override
    protected Profile mergePredictions(OutputEncoding[] allPredictions) {
        Profile ret = new ProfileMatrix(3, this.current.input.length(), 0);

        for (int i = 0; i < allPredictions.length; i++) {
            Profile prediction = allPredictions[i].getProfile();
            List<Double> expertWeights = this.getExpertWeights(this.experts.get(i));

            // Per ogni colonna della predizione: prendi la colonna, prendi il peso associato
            // alla colonna (che apparterrà ad una delle finestre), moltiplica la colonna per
            // il peso e setta la colonna pesata.
            for (int col = 0; col < prediction.length(); col++) {
                double[] colValues = prediction.getColumn(col);

                int currWindow = col / this.current.windowSize;
                if (currWindow >= this.current.windowNumber) // L'ultima finestra è più lunga perché ha il resto dei residui
                    currWindow = this.current.windowNumber - 1;
                double currWeight = expertWeights.get(currWindow);

                for (int j = 0; j < colValues.length; j++)
                    colValues[j] *= currWeight;

                prediction.setColumn(col, colValues);
            }

            ret.add(prediction);
        }
        ret.normalizeColumns();

        return ret;
    }
}
